/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.factories.impl;

import android.content.Context;

import com.hercules.android.receiver.AndroidReceiverErrorHandler;
import com.hercules.android.receiver.DebugHelper;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.ReceiverUtil;
import com.hercules.android.receiver.app.R;
import com.hercules.android.receiver.app.DisplayApplicationActivity.ConfigurationException;
import com.hercules.android.receiver.connection.BaseServerConnection;
import com.hercules.android.receiver.connection.ConnectionErrorHandler;
import com.hercules.android.receiver.connection.ServerConnection;
import com.hercules.android.receiver.connection.http.HTTPServerConnection;
import com.hercules.android.receiver.connection.parser.json.JSONMessageParser;
import com.hercules.android.receiver.connection.tcp.TCPServerConnection;
import com.hercules.android.receiver.errorhandlers.DefaultAndroidReceiverErrorHandler;
import com.hercules.android.receiver.errorhandlers.DefaultConnectionErrorHandler;
import com.hercules.android.receiver.errorhandlers.DefaultXRECommandErrorHandler;
import com.hercules.android.receiver.factories.AndroidReceiverFactory;
import com.hercules.android.receiver.factories.AndroidReceiverFactoryException;
import com.hercules.android.receiver.handlers.commands.DefaultXREViewFinderUtility;
import com.hercules.android.receiver.handlers.commands.XREAggregateCommandHandler;
import com.hercules.android.receiver.handlers.commands.XRECommandHandler;
import com.hercules.android.receiver.handlers.commands.XREConnectCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREConnectRejectedCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREDeleteCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREGetCommandHandler;
import com.hercules.android.receiver.handlers.commands.XRENewCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREObjectCommandHandler;
import com.hercules.android.receiver.handlers.commands.XRERedirectCommandHandler;
import com.hercules.android.receiver.handlers.commands.XRERestartCommandHandler;
import com.hercules.android.receiver.handlers.commands.XRESetCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREShutdownCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREStartTimerCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREViewFinderUtility;
import com.hercules.android.receiver.handlers.commands.call.ObjectEncryptor;
import com.hercules.android.receiver.handlers.commands.call.ObjectPersistence;
import com.hercules.android.receiver.handlers.commands.call.XREApplicationCallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.XRECallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.XRECallCommandViewFinderUtility;
import com.hercules.android.receiver.handlers.commands.call.XRECommandSequenceCallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.XREServiceProxyCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.XRESoundCallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.XREVideoCallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.XREViewCallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.animation.XREAnimationCallCommandHandler;
import com.hercules.android.receiver.handlers.commands.call.encryption.DefaultObjectEncryptor;
import com.hercules.android.receiver.handlers.commands.call.persistence.DefaultObjectPersistence;
import com.hercules.android.receiver.handlers.events.EventRaiser;
import com.hercules.android.receiver.handlers.events.XREGenerateEventCallCommandHandler;
import com.hercules.android.receiver.handlers.events.binder.EventListenerBinderImpl;
import com.hercules.android.receiver.handlers.events.eventraiser.EventRaiserImpl;
import com.hercules.android.receiver.handlers.events.keymappingservice.KeyMappingServiceImpl;
import com.hercules.android.receiver.model.commands.XRECallCommand;
import com.hercules.android.receiver.model.commands.XREConnectCommand;
import com.hercules.android.receiver.model.commands.XREConnectRejectedCommand;
import com.hercules.android.receiver.model.commands.XREDeleteCommand;
import com.hercules.android.receiver.model.commands.XREGetCommand;
import com.hercules.android.receiver.model.commands.XRENewCommand;
import com.hercules.android.receiver.model.commands.XRERedirectCommand;
import com.hercules.android.receiver.model.commands.XRERestartCommand;
import com.hercules.android.receiver.model.commands.XRESetCommand;
import com.hercules.android.receiver.model.commands.XREShutdownCommand;
import com.hercules.android.receiver.model.commands.XREStartTimerCommand;
import com.hercules.android.receiver.model.objects.XREApplication;
import com.hercules.android.receiver.model.objects.XRECommandSequence;
import com.hercules.android.receiver.model.objects.XREServiceProxy;
import com.hercules.android.receiver.model.objects.XRESound;
import com.hercules.android.receiver.model.objects.XREVideo;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.painting.XREInitPainter;
import com.hercules.android.receiver.painting.XREObjectPainter;
import com.hercules.android.receiver.painting.XREObjectPainterUtils;
import com.hercules.android.receiver.painting.views.AndroidXREView;
import com.hercules.android.receiver.util.evaluators.ExpressionEvaluator;
import com.hercules.android.receiver.util.evaluators.PathEvaluator;
import com.hercules.android.receiver.util.evaluators.impl.XREExpressionEvaluator;
import com.hercules.android.receiver.util.evaluators.impl.XREPathEvaluator;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

/**
 * Implementation of {@link AndroidReceiverFactory}, that uses context and resource file to create objects with given
 * keys. It will use resource R.raw.factory to load factory - specific properties.
 * <p>
 * AndroidAssembly1 this class was added in Android Assembly 1
 * <p>
 * <b>Thread safety:</b> This class has mutable fields, but in scope of it's usage it will be effectively thread safe
 *
 * @author 7realm
 * @version 1.0
 */
public class ContextAndroidReceiverFactory implements AndroidReceiverFactory {
    /** Represents logger for this class. */
    private static final Logger LOGGER = LogUtil.getLogger(ContextAndroidReceiverFactory.class.getSimpleName());
    /**
     * Represents stored android {@link Context} that will allow operations on files.
     * <p>
     * Is set in constructor, cannot be null. Used in factory methods.
     */
    private final Context context;

    /**
     * Represents properties for this factory, will be loaded on first request.
     * <p>
     * Will be null before they are loaded. After that it will be not null.
     */
    private Properties properties;

    /**
     * Constructor for class ContextAndroidReceiverFactory.
     *
     * @param context the android context
     */
    public ContextAndroidReceiverFactory(Context context) {
        if (context == null) {
            throw new IllegalArgumentException("Argument 'context' value should not be null.");
        }

        this.context = context;
    }

    /**
     * Creates an instance of the {@link XREAggregateCommandHandler} using the given key.
     *
     * @param key a key indicating the {@link XREAggregateCommandHandler} to create
     * @return the {@link XREAggregateCommandHandler} instance
     * @throws AndroidReceiverFactoryException if there is an error while creating the instance or key is not recognized
     */
    public XREAggregateCommandHandler createXREAggregateCommandHandler(String key)
        throws AndroidReceiverFactoryException {
        String methodName = "createXREAggregateCommandHandler";
        LogUtil.enter(LOGGER, this, methodName, key);

        try {
            if ("commandHandler".equals(key)) {
                loadProperties();

                // prepare event raiser
                EventRaiserImpl eventRaiser = new EventRaiserImpl();

                // prepare event listener binder
                EventListenerBinderImpl binder = new EventListenerBinderImpl();
                binder.setEventRaiser(eventRaiser);
                KeyMappingServiceImpl keyMappingService = new KeyMappingServiceImpl();
                keyMappingService.setContext(context);
                keyMappingService.setResourceId(R.raw.mapping);
                binder.setKeyMappingService(keyMappingService);

                // prepare object painters
                XREObjectPainter applicationPainter = new XREInitPainter();
                Map<String, XREObjectPainter> objectPainters = XREObjectPainterUtils.getDefaultPaintersMap();

                // prepare path and expressions evaluators
                PathEvaluator pathEvaluator = new XREPathEvaluator(XREPathEvaluator.class.getSimpleName());
                ExpressionEvaluator expressionEvaluator =
                    new XREExpressionEvaluator(XREExpressionEvaluator.class.getSimpleName(), pathEvaluator);

                // prepare error handler
                DefaultXRECommandErrorHandler errorHandler = new DefaultXRECommandErrorHandler(context);
                String fileName = properties.getProperty("commands.errors.file");
                errorHandler.setFileName(fileName);
                if (fileName != null && !getBoolean("commands.errors.append")) {
                    context.deleteFile(fileName);
                }

                // create XREViewFinderUtility
                XREViewFinderUtility affectedViewFinder = new DefaultXREViewFinderUtility();

                // create and configure XREConnectCommandHandler
                XREConnectCommandHandler connectCommandHandler = new XREConnectCommandHandler();
                connectCommandHandler.setApplicationPainter(applicationPainter);

                // create and configure XREShutdownCommandHandler
                XREShutdownCommandHandler shutdownCommandHandler = new XREShutdownCommandHandler();
                shutdownCommandHandler.setApplicationPainter(applicationPainter);

                // create XREConnectRejectedCommandHandler
                XREConnectRejectedCommandHandler connectRejectedCommandHandler = new XREConnectRejectedCommandHandler();

                // create XRERedirectCommandHandler
                XRERedirectCommandHandler redirectCommandHandler = new XRERedirectCommandHandler();

                // create and configure XRERestartCommandHandler
                XRERestartCommandHandler restartCommandHandler = new XRERestartCommandHandler();
                restartCommandHandler.setApplicationPainter(applicationPainter);

                // create and configure XRENewCommandHandler
                XRENewCommandHandler newCommandHandler = new XRENewCommandHandler();
                newCommandHandler.setBinder(binder);
                newCommandHandler.setObjectPainters(objectPainters);
                newCommandHandler.setAffectedViewFinder(affectedViewFinder);

                // create and configureXRESetCommandHandler
                XRESetCommandHandler setCommandHandler = new XRESetCommandHandler();
                setCommandHandler.setBinder(binder);
                setCommandHandler.setExpressionEvaluator(expressionEvaluator);
                setCommandHandler.setObjectPainters(objectPainters);
                setCommandHandler.setAffectedViewFinder(affectedViewFinder);

                // create and configure XREDeleteCommandHandler
                XREDeleteCommandHandler deleteCommandHandler = new XREDeleteCommandHandler();
                deleteCommandHandler.setObjectPainters(objectPainters);
                deleteCommandHandler.setAffectedViewFinder(affectedViewFinder);

                // AndroidAssembly2 added call command handler
                XRECallCommandHandler callCommandHandler =
                    createCallCommandHandler(eventRaiser, objectPainters, affectedViewFinder);

                // AndroidAssembly3 added get command handler
                XREGetCommandHandler getCommandHandler = new XREGetCommandHandler();
                getCommandHandler.setEventRaiser(eventRaiser);

                XREStartTimerCommandHandler startTimerCommandHandler = new XREStartTimerCommandHandler();
                
                // prepare list of command handlers
                Map<String, XRECommandHandler> commandHandlers = new HashMap<String, XRECommandHandler>();
                commandHandlers.put(XREConnectCommand.COMMAND_NAME, connectCommandHandler);
                commandHandlers.put(XREConnectRejectedCommand.COMMAND_NAME, connectRejectedCommandHandler);
                commandHandlers.put(XREShutdownCommand.COMMAND_NAME, shutdownCommandHandler);
                commandHandlers.put(XRERedirectCommand.COMMAND_NAME, redirectCommandHandler);
                commandHandlers.put(XRERestartCommand.COMMAND_NAME, restartCommandHandler);
                commandHandlers.put(XRENewCommand.COMMAND_NAME, newCommandHandler);
                commandHandlers.put(XRESetCommand.COMMAND_NAME, setCommandHandler);
                commandHandlers.put(XREDeleteCommand.COMMAND_NAME, deleteCommandHandler);
                commandHandlers.put(XRECallCommand.COMMAND_NAME, callCommandHandler);
                commandHandlers.put(XREGetCommand.COMMAND_NAME, getCommandHandler);
                commandHandlers.put(XREStartTimerCommand.COMMAND_NAME, startTimerCommandHandler);

                // create XREAggregateCommandHandler
                XREAggregateCommandHandler aggregateCommandHandler = new XREAggregateCommandHandler();

                // configure XREAggregateCommandHandler
                aggregateCommandHandler.setCommandHandlers(commandHandlers);
                aggregateCommandHandler.setPathEvaluator(pathEvaluator);
                aggregateCommandHandler.setErrorHandler(errorHandler);

                // set event raise for resource events
                AndroidXREView.setEventRaiser(eventRaiser);
                AndroidXREView.setKeyMappingService(keyMappingService);
                
                return LogUtil.exit(LOGGER, this, methodName, aggregateCommandHandler);
            }
            throw new AndroidReceiverFactoryException("Failed to create aggregate command handler with key '" + key
                + "'.");
        } catch (AndroidReceiverFactoryException e) {
            throw LogUtil.error(LOGGER, this, methodName, e);
        }
    }

    /**
     * Creates {@link XRECallCommand} handler.
     *
     * @param eventRaiser the {@link EventRaiser} implementation
     * @param objectPainters the object painters
     * @param affectedViewFinder the affected view finder utility
     * @return the created {@link XRECallCommandHandler}
     */
    private static XRECallCommandHandler createCallCommandHandler(EventRaiserImpl eventRaiser,
        Map<String, XREObjectPainter> objectPainters, XREViewFinderUtility affectedViewFinder) {
        // prepare handler for call commands with method generateEvent
        XREGenerateEventCallCommandHandler generateEventHandler = new XREGenerateEventCallCommandHandler(eventRaiser);

        // prepare handler for call commands with method animate
        XREAnimationCallCommandHandler animationHandler = new XREAnimationCallCommandHandler();
        animationHandler.setObjectPainters(objectPainters);
        animationHandler.setAffectedViewFinder(affectedViewFinder);
        animationHandler.setEventRaiser(eventRaiser);

        // prepare object encryptor
        ObjectEncryptor encryptor = new DefaultObjectEncryptor();

        // prepare object persistence
        ObjectPersistence persistence = new DefaultObjectPersistence();

        // prepare call command handler for XREApplication
        XREApplicationCallCommandHandler applicationHandler = new XREApplicationCallCommandHandler();
        applicationHandler.setPersistence(persistence);
        applicationHandler.setEncryptor(encryptor);
        applicationHandler.setEventRaiser(eventRaiser);
        applicationHandler.setEventCallCommandHandler(generateEventHandler);

        // prepare call command handler for XRECommandSequence
        XRECommandSequenceCallCommandHandler sequenceHandler = new XRECommandSequenceCallCommandHandler();

        // prepare call command handler for XRESound
        XRESoundCallCommandHandler soundHandler = new XRESoundCallCommandHandler();

        // prepare call command handler for XREVideo
        XREVideoCallCommandHandler videoHandler = new XREVideoCallCommandHandler();
        videoHandler.setEventRaiser(eventRaiser);

        // prepare call command handler for XREView
        XREViewCallCommandHandler viewHandler = new XREViewCallCommandHandler();
        viewHandler.setAffectedViewFinder(new XRECallCommandViewFinderUtility());
        viewHandler.setObjectPainters(objectPainters);
        viewHandler.setEventRaiser(eventRaiser);
        viewHandler.setEventCallCommandHandler(generateEventHandler);
        viewHandler.setAnimationCallCommandHandler(animationHandler);

        // prepare call command handler for XREServiceProxy
        XREServiceProxyCommandHandler serviceProxyHandler = new XREServiceProxyCommandHandler();
        
        // prepare call command handlers
        Map<String, XREObjectCommandHandler> callCommandHandlers = new HashMap<String, XREObjectCommandHandler>();
        callCommandHandlers.put(XREApplication.class.getSimpleName(), applicationHandler);
        callCommandHandlers.put(XRECommandSequence.class.getSimpleName(), sequenceHandler);
        callCommandHandlers.put(XRESound.class.getSimpleName(), soundHandler);
        callCommandHandlers.put(XREVideo.class.getSimpleName(), videoHandler);
        callCommandHandlers.put(XREView.class.getSimpleName(), viewHandler);
        callCommandHandlers.put(XREServiceProxy.class.getSimpleName(), serviceProxyHandler);

        // create XRECallCommandHandler
        XRECallCommandHandler callCommandHandler = new XRECallCommandHandler();
        callCommandHandler.setCallCommandHandlers(callCommandHandlers);
        return callCommandHandler;
    }

    /**
     * Creates an instance of the {@link AndroidReceiverErrorHandler} using the given key.
     *
     * @param key a key indicating the {@link AndroidReceiverErrorHandler} to create
     * @return the {@link AndroidReceiverErrorHandler} instance
     * @throws AndroidReceiverFactoryException if there is an error while creating the instance or key is not recognized
     */
    public AndroidReceiverErrorHandler createErrorHandler(String key) throws AndroidReceiverFactoryException {
        String methodName = "createErrorHandler";
        LogUtil.enter(LOGGER, this, methodName, key);

        if ("errorHandler".equals(key)) {
            return LogUtil.exit(LOGGER, this, methodName, new DefaultAndroidReceiverErrorHandler());
        }
        throw LogUtil.error(LOGGER, this, methodName, new AndroidReceiverFactoryException(
            "Failed to create error handler with key '" + key + "'."));
    }

    /**
     * Creates an instance of the {@link ServerConnection} using the given key.
     *
     * @param key a key indicating the ServerConnection to create
     * @return the {@link ServerConnection} instance
     * @throws AndroidReceiverFactoryException if there is an error while creating the instance or key is not recognized
     */
    public ServerConnection createServerConnection(String key) throws AndroidReceiverFactoryException {
        String methodName = "createServerConnection";
        LogUtil.enter(LOGGER, this, methodName, key);

        try {
            loadProperties();

            // common instances
            JSONMessageParser parser = new JSONMessageParser();
            ConnectionErrorHandler exceptionHandler = new DefaultConnectionErrorHandler();

            BaseServerConnection<?> connection;
            if ("tcpConnection".equals(key)) {
                connection = new TCPServerConnection();
            } else if ("httpConnection".equals(key)) {
                connection = new HTTPServerConnection();
                ((HTTPServerConnection) connection).setEmptyMessagesInterval(getInt(
                    "connection.http.emptyMessage.interval", 2000));
            } else {
                throw new AndroidReceiverFactoryException("Failed to create server connection with key '" + key + "'.");
            }

            // set common fields
            connection.setExceptionHandler(exceptionHandler);
            connection.setParser(parser);
            connection.setReconnectAttempts(getInt("connection.reconnect.attempts", 5));
            connection.setReconnectAttemptsInterval(getInt("connection.reconnect.attempts.interval", 1000));
            connection.setReconnectWaitInterval(getInt("connection.reconnect.wait.interval", 1000));

            return LogUtil.exit(LOGGER, this, methodName, connection);
        } catch (AndroidReceiverFactoryException e) {
            throw LogUtil.error(LOGGER, this, methodName, e);
        }
    }

    /**
     * Gets integer value from properties.
     *
     * @param name the name of the property
     * @param defaultValue default value
     * @return the property value
     * @throws AndroidReceiverFactoryException if property value cannot be parsed
     */
    private int getInt(String name, int defaultValue) throws AndroidReceiverFactoryException {
        String value = properties.getProperty(name);
        if (value == null) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new AndroidReceiverFactoryException(
                "Failed to parse value '" + value + "' of property " + name + ".", e);
        }
    }

    /**
     * Gets boolean value from properties.
     * <p>
     * AndroidAssembly2 new method
     *
     * @param propertyName the name of the property
     * @return the property value
     * @throws AndroidReceiverFactoryException if property value cannot be parsed
     */
    private boolean getBoolean(String propertyName) throws AndroidReceiverFactoryException {
        String value = properties.getProperty(propertyName);
        if (value == null || "off".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
            return false;
        }

        if ("on".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)) {
            return true;
        }

        throw new AndroidReceiverFactoryException("Failed to parse boolean value '" + value + "' of property "
            + propertyName + ".");
    }

    /**
     * Loads properties from resource.
     *
     * @throws AndroidReceiverFactoryException if error occurs while loading properties
     */
    private void loadProperties() throws AndroidReceiverFactoryException {
        if (properties == null) {
            try {
                properties = ReceiverUtil.loadProperties(context, R.raw.factory);

                // set debug properties
                DebugHelper.commandsInFile = properties.getProperty("debug.commands.in.file");
                DebugHelper.commandsOutFile = properties.getProperty("debug.commands.out.file");
                DebugHelper.testNumber = getInt("debug.test", 0);
                DebugHelper.drawViewBounds = getBoolean("debug.view.bounds");

                // check global debug flag
                if (!getBoolean("debug")) {
                    DebugHelper.switchOff();
                }

            } catch (IOException e) {
                throw new AndroidReceiverFactoryException("I/O error occurred while loading properties.", e);
            } catch (ConfigurationException e) {
                throw new AndroidReceiverFactoryException("Resource R.raw.factory does not exist.", e);
            }
        }
    }
}
